package com.KOL.present.game;

import com.KOL.model.Enemy;
import com.KOL.model.TiledEntity;
import com.KOL.model.character.PlayerCharacter;
import com.KOL.present.ChunkDisplay;
import com.KOL.present.Section;

import com.sun.j3d.utils.timer.J3DTimer;
import com.util.ImagesLoader;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.image.BufferedImage;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JPanel;

public class Game extends JPanel implements Runnable {
    private static final int PWIDTH = 1280; //TODO: pull from config
    private static final int PHEIGHT = 720; //TODO: pull from config

    private static final int X_HALF = 612;
    private static final int Y_HALF = 390;

    private static final int NO_DELAYS_PER_YIELD = 16;
    /* Number of frames with a delay of 0 ms before the animation thread yields
     to other running threads. */
    private static final int MAX_FRAME_SKIPS = 5;
    // no. of frames that can be skipped in any one animation loop
    // i.e the games state is updated but not rendered
    private static final Color SKY_BOX_COLOR = new Color(0.0f,0.0f,0.0f);//Color(0.17f, 0.87f, 1.0f);

    private Thread animator;
    private Timer mouseTimer;
    private volatile boolean running = false;
    private volatile boolean isPaused = false;

    private int mouseX;
    private int mouseY;

    private long period;
    private GameWindow window;
    private ChunkDisplay currentChunk;
    private ImagesLoader imsLoader;

    private PlayerCharacter hero;
    private Enemy enemies[];

    private Font msgsFont;
    private FontMetrics metrics;

    private Graphics dbg;
    private Image dbImage = null;

    private boolean showTitleScreen;
    private BufferedImage titleScreen;

    public Game(GameWindow window, long period,ImagesLoader imsLoader,ChunkDisplay chunk, PlayerCharacter hero) {
        this.window = window;
        this.period = period;
        this.hero = hero;
        this.currentChunk = chunk;
        this.mouseX = 0;
        this.mouseY = 0;

        setDoubleBuffered(false);
        setBackground(Color.black);
        setPreferredSize( new Dimension(PWIDTH, PHEIGHT));

        setFocusable(true);
        requestFocus();

	addKeyListener( new KeyAdapter() {
        @Override
        public void keyPressed(KeyEvent e)
        { processKey(e); }
        });

        addMouseMotionListener(new MouseMotionAdapter() {
        @Override
        public void mouseMoved(MouseEvent e) {
            mouseX = e.getXOnScreen();
            mouseY = e.getYOnScreen();
        }
        @Override
        public void mouseDragged(MouseEvent e) {
            mouseX = e.getXOnScreen();
            mouseY = e.getYOnScreen();
        }
        });
        addMouseListener(new MouseAdapter() {
        @Override
        public void mousePressed(MouseEvent e) {
            mouseTimer = new Timer();
            mouseTimer.schedule(new ReProcessMouse(e), 50,120);
        }
        @Override
        public void mouseReleased(MouseEvent e) {
            mouseTimer.cancel();
            processMouseReleased(e);
        }
        });

        this.imsLoader = imsLoader;
        //TODO: Add sounds new SoundClipManager(SOUND_FILE);

        createWorld();

        titleScreen = imsLoader.getImage("title");
        showTitleScreen = false; //TODO: replace placeholder
        isPaused = false;

        msgsFont = new Font("SansSerif", Font.BOLD, 24);
        metrics = this.getFontMetrics(msgsFont);
    }

    class ReProcessMouse extends TimerTask{
        private MouseEvent mouseEvent;
        public ReProcessMouse(MouseEvent e) {
            mouseEvent = e;
        }
        public void run() {
            processMousePressed(mouseEvent);
        }
    }

    private void createWorld() {
        enemies = new Enemy[currentChunk.getTotalEnemiesForSection()];

        for(int i = 0;i < enemies.length; i++) {
            // TODO: add enemies     enemies[i] = new Enemy(..);
        }

        currentChunk.addEntities(hero, enemies);
    }

    private void processMouseReleased(MouseEvent e) {
        if(!isPaused) {
            if(e.getButton() == MouseEvent.BUTTON1) {
                hero.standStill();
            }
        }
    }

    private void processMousePressed(MouseEvent e) {
        if(!isPaused) {
            if(e.getButton() == MouseEvent.BUTTON1) {
                if (mouseX < X_HALF && mouseY < Y_HALF)
                    hero.move(TiledEntity.NW);
                else if (mouseX > X_HALF && mouseY < Y_HALF)
                    hero.move(TiledEntity.NE);
                else if (mouseX > X_HALF && mouseY > Y_HALF)
                    hero.move(TiledEntity.SE);
                else if (mouseX < X_HALF && mouseY > Y_HALF)
                    hero.move(TiledEntity.SW);
            }else if(e.getButton() == MouseEvent.BUTTON2) {
                //TODO: Implement pick up world drop item
            }
        }
    }

    private void processKey(KeyEvent e) {
        int keyCode = e.getKeyCode();

        if ((keyCode == KeyEvent.VK_ESCAPE) ||
            (keyCode == KeyEvent.VK_END) ||
            ((keyCode == KeyEvent.VK_C) && e.isControlDown()) )
          running = false;


        if (keyCode == KeyEvent.VK_P) {
          if (isPaused)
            isPaused = false;
          else
           isPaused = true;
        }

        if (!isPaused) {

          if (keyCode == KeyEvent.VK_Q)
            hero.move(TiledEntity.NW);
          else if (keyCode == KeyEvent.VK_E)
            hero.move(TiledEntity.NE);
          else if (keyCode == KeyEvent.VK_D)
            hero.move(TiledEntity.SE);
          else if (keyCode == KeyEvent.VK_A)
            hero.move(TiledEntity.SW);
          else if (keyCode == KeyEvent.VK_W)
            hero.standStill();
          else if (keyCode == KeyEvent.VK_S)
            hero.tryPickupItem();
        }
    }

    @Override
    public void addNotify() {
        super.addNotify();
        startGame();
    }

    private void startGame() {
        if (animator == null || !running) {
            animator = new Thread(this);
            animator.start();
        }
    }

    public void resumeGame() {
        if (isPaused)
            isPaused = false;
    }

    public void pauseGame()
    { isPaused = true;   }

    public void stopGame()
    {  running = false;   }

    public void run() {
        long beforeTime, afterTime, timeDiff, sleepTime;
        long overSleepTime = 0L;
        int noDelays = 0;
        long excess = 0L;

        beforeTime = J3DTimer.getValue();

	running = true;

	while(running) {
            gameUpdate();
            gameRender();
            paintScreen();

            afterTime = J3DTimer.getValue();
            timeDiff = afterTime - beforeTime;
            sleepTime = (period - timeDiff) - overSleepTime;

            if (sleepTime > 0) {
                try {
                  Thread.sleep(sleepTime/1000000L);  // nano -> ms
                }
                catch(InterruptedException ex){}
                overSleepTime = (J3DTimer.getValue() - afterTime) - sleepTime;
            } else {    // sleepTime <= 0; the frame took longer than the period
                excess -= sleepTime;  // store excess time value
                overSleepTime = 0L;

                if (++noDelays >= NO_DELAYS_PER_YIELD) {
                    Thread.yield();   // give another thread a chance to run
                    noDelays = 0;
                }
            }

            beforeTime = J3DTimer.getValue();

            /* If frame animation is taking too long, update the game state
             without rendering it, to get the updates/sec nearer to
             the required FPS. */
            int skips = 0;
            while((excess > period) && (skips < MAX_FRAME_SKIPS)) {
                excess -= period;
                gameUpdate();    // update state but don't render
                skips++;
            }
	}
        
        System.exit(0);   // so window disappears
    }

    private void gameUpdate() {
        if (!isPaused) {
          for(Enemy enemy : enemies)
            enemy.update();
        }
    }

    private void gameRender() {
        if (dbImage == null){
            dbImage = createImage(PWIDTH, PHEIGHT);

            if (dbImage == null) {
                System.err.println("dbImage is null");
                 return;
            } else
                dbg = dbImage.getGraphics();
        }

        dbg.setColor(SKY_BOX_COLOR);
        dbg.fillRect(0, 0, PWIDTH, PHEIGHT);

        currentChunk.draw(dbg);

        if (hero.isDead())
            isDeadMessage(dbg);

        if (showTitleScreen)
            dbg.drawImage(titleScreen, (PWIDTH-titleScreen.getWidth())/2, (PHEIGHT-titleScreen.getHeight())/2, null);
    }

    private void isDeadMessage(Graphics g) {
        String msg = "You're Dead.\n You were slain by " + hero.wasKilledBy();

	int x = (PWIDTH - metrics.stringWidth(msg))/2;
	int y = (PHEIGHT - metrics.getHeight())/2;
	g.setColor(Color.red);
        g.setFont(msgsFont);
	g.drawString(msg, x, y);
        g.setColor(Color.black);
    }

    private void paintScreen() {
        Graphics g;

        try {
            g = this.getGraphics();
            if ((g != null) && (dbImage != null))
            g.drawImage(dbImage, 0, 0, null);

            Toolkit.getDefaultToolkit().sync();
            g.dispose();
        } catch (Exception e) {
            System.out.println("Graphics context error: " + e);
        }
    }
}
